Repository navigation
Fixed bogus lookup logic within list-land .TransformAsync(),… - #1171
Conversation
…ocessing changes with unspecified indexes, which resulted in `Remove` and `RemoveRange` changes with unspecified indexes to be ignored completely.
ed51f2a to
18b0f53
Compare
RemoveWithoutIndex and RemoveRangeWithoutIndex were added by #1171 and dropped while rebasing the global usings onto it.
dwcullop
left a comment
There was a problem hiding this comment.
Fix looks right to me. Matching on EqualityComparer<TSource>.Default lines up with how TransformedItemContainer already defines equality.
The synchronous Transform has the same bug, though, so it'd be good to fix both here:
List/Internal/Transformer.cs:146and:165match indexless removes withReferenceEquals(t.Source, change.Current). For value types that's always false because both sides get boxed, so an indexlessRemove/RemoveRangeofints is silently dropped, which is exactly #1169.- The
RemoveRangebranch there (Where(t => item.Range.Any(...))intoRemoveMany) also removes every duplicate of an item, not one per entry in the range. TransformFixture.RemoveWithoutIndexonly usesPerson, so nothing catches it. Mirroring your two newinttests againstTransformwould.
As it stands sync and async also disagree on what "the same item" means (reference identity vs Equals), which seems worth closing while we're here.
Small nit on the lookup itself: the Select((pair, index) => (sourceItem, (int?)index)).FirstOrDefault(...).index trick takes a second read, allocates per remove, and is duplicated. A plain index loop in one helper that both Transformer and TransformAsync call would be simpler and keeps the two operators from drifting again.
79fb9b0 to
43f46bc
Compare
|
Fair point about putting equivalent fixes on I'm not too fussed about the optimalness of the index lookup, when that's SUCH a "this really shouldn't be a thing" scenario, but looking at it, I realized there were actually several OTHER inconsistencies (for other change types) in how index lookups are being done, across both operators. So yeah, I just added a helper method and refactored it into both operators, everywhere an index lookup is needed. There's still inconsistencies in how the operators handle an index lookup that FAILS (some change types will throw, some will just silently ignore the change) but I opted to just preserve the existing logic, rather than expand scope any further. A failed index lookup really is just an extension of changeset corruption, which is not really this PR's problem to deal with. |
…d logic for handling index lookup, when indexes are missing from changesets, into a shared allocation-free helper method.
43f46bc to
4d81cb1
Compare
| { | ||
| var toRemove = transformed.Where(t => ReferenceEquals(t.Source, t)).ToArray(); | ||
| transformed.RemoveMany(toRemove); | ||
| foreach (var removedItem in item.Range) |
There was a problem hiding this comment.
This outer foreach (var removedItem in item.Range) repeats the whole removal once per item in the range. With duplicates it removes too much: SourceList [1,2,1,2], RemoveIndex(), RemoveRange(0,2) gives an empty list instead of [1,2]. Without duplicates it's still very slow: removing 500 of 1000 took about 1.4s here versus 1ms for sync Transform. I think the outer loop just needs to go.
There was a problem hiding this comment.
Yeah, I'm not sure how I ended up here. Almost looks like a merge conflict.
Fixed.
| { | ||
| var toRemove = transformed.Where(t => item.Range.Any(current => ReferenceEquals(t.Source, current))); | ||
| transformed.RemoveMany(toRemove); | ||
| var manyToRemove = new List<TransformedItemContainer>(capacity: item.Range.Count); |
There was a problem hiding this comment.
Since FindItemIndex already gives us the index, can we just RemoveAt(index) per item rather than collecting and calling RemoveMany? RemoveMany does the lookup again with Join, OrderBy and Duplicates and copies the ChangeAwareList while enumerating it. Same change in TransformAsync.
There was a problem hiding this comment.
The idea was supposed to be that we don't break apart the range change into a series of individual changes, but yeah, that's not what's happening anyway. The .RemoveMany() call is binding to an IList<T> extension method, not any method on ChangeAwareList<T> that would be able to optimize.
Fixed.
| var previousIndex = change.PreviousIndex; | ||
|
|
||
| if (previousIndex < 0) | ||
| previousIndex = transformed.FindItemIndex(change.Previous.Value); |
There was a problem hiding this comment.
If the previous item isn't found, this falls through to transformed[-1] and throws ArgumentOutOfRangeException. Sync throws UnspecifiedIndexException here. Can we throw the same thing so the two match?
There was a problem hiding this comment.
Per my initial comment, I left in all the existing discrepancies in failed lookup behavior intact. They're not part of #1169, and and how an operator responds to being given corrupt changesets is really undefined behavior. I'm down with cleaning it up, but not here.
| results.HasCompleted.Should().BeTrue("the source, and all asynchronous operations, have completed"); | ||
| } | ||
|
|
||
| // Mirrors coverage for https://github.com/reactivemarbles/DynamicData/issues/1169 |
There was a problem hiding this comment.
Could we add a duplicate-values case (e.g. [1,2,1,2] with an unindexed RemoveRange of [1,2]) and an unindexed Replace for both operators? The duplicates test would have caught the extra loop in the async version. Also, small thing: the // Covers / // Mirrors coverage comments and the trailing whitespace are still there.
There was a problem hiding this comment.
I just added some duplicate items to the existing tests. It's not like we lose any coverage by dropping the no-duplicates scenario.
Also, small thing: the // Covers / // Mirrors coverage comments and the trailing whitespace are still there.
Uhhh, yeah, why wouldn't they be? And what trailing whitespace?
|
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
… for processing changes with unspecified indexes, which resulted in
RemoveandRemoveRangechanges with unspecified indexes to be ignored completely.